package com.gitee.zycra;

import com.gitee.zycra.config.ConfigLoader;
import com.gitee.zycra.model.Column;
import com.gitee.zycra.model.GeneratorConfig;
import com.gitee.zycra.utils.DbUtils;
import com.gitee.zycra.utils.GeneratorUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zycra
 */
@Mojo(name = "generate")
public class GeneratorMojo extends AbstractMojo {

    @Parameter(defaultValue = "${basedir}", readonly = true)
    private File basedir;
    @Parameter(defaultValue = "/src/main/resources/generator.properties", readonly = true)
    private String configLocation;

    private final String javaTail = ".java";

    private final String xmlTail = ".xml";

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        String propertiesFilePath = basedir.getAbsolutePath() + configLocation;
        GeneratorConfig config = ConfigLoader.loanConfig(propertiesFilePath);
        Map<String, String> tableCommentMap = DbUtils.selectTableComment(config);
        Map<String, List<Column>> columnMap = DbUtils.selectAllColumns(config);
        Map<String, Map<String, List<String>>> uniqueKeyColumnMap = DbUtils.selectUniqueKeyColumnMap(config);
        Map<String, String> primaryKeyColumnMap = DbUtils.selectPrimaryKeyColumnMap(config);
        String parentPath = basedir.getParentFile().getAbsolutePath();
        String modelPath = config.getModelPath();
        String modelPackage = getPackage(modelPath);
        String mapperInterfacePath = config.getMapperInterfacePath();
        String mapperInterfacePackage = getPackage(mapperInterfacePath);
        String mapperXmlPath = config.getMapperXmlPath();
        List<String> insertIgnoreList = config.getInsertIgnoreList();
        for (Map.Entry<String, List<Column>> entry : columnMap.entrySet()) {
            String tableName = entry.getKey();
            String tableComment = tableCommentMap.get(tableName);
            String modelClassName = GeneratorUtils.convertToJavaName(tableName, true);
            String lowerModelClass = GeneratorUtils.convertToJavaName(tableName, false);
            String mapperClassName = modelClassName + "Mapper";
            List<Column> columnList = entry.getValue();
            Map<String, List<String>> uniqueKeyGroupMap = uniqueKeyColumnMap.get(tableName);
            String primaryKeyColumnName = primaryKeyColumnMap.get(tableName);
            // 生成实体
            generateDO(parentPath + modelPath, modelPackage, modelClassName, columnList, tableComment);
            getLog().info(parentPath + modelPath + modelClassName + javaTail);
            // 生成Mapper接口
            generateMapperInterface(parentPath + mapperInterfacePath, mapperInterfacePackage, mapperClassName, modelClassName,
                    lowerModelClass, columnList, uniqueKeyGroupMap, primaryKeyColumnName, tableComment, modelPackage);
            getLog().info(parentPath + mapperInterfacePath + mapperClassName + javaTail);
            // 生成xml
            generateMapperXml(parentPath + mapperXmlPath, mapperInterfacePackage, mapperClassName, modelPackage + "." + modelClassName, columnList,
                    tableName, uniqueKeyGroupMap, primaryKeyColumnName, insertIgnoreList);
            getLog().info(parentPath + mapperXmlPath + mapperClassName + xmlTail);
        }
    }

    private String getPackage(String path) {
        return path.substring(path.indexOf("src/main/java/") + 14, path.length() - 1).replace("/", ".");
    }

    private String repeatBlank(int size) {
        if (size == 1) {
            return " ";
        }
        String halfBlank = repeatBlank(size >> 1);
        String allBlank = halfBlank + halfBlank;
        return (size & 1) == 1 ? allBlank + " " : allBlank;
    }

    private void generateDO(String filePath, String packageName, String className, List<Column> colunmList, String tableComment) throws MojoFailureException {
        String head = getPojoHead(packageName);
        String classHead = getPojoClassHead(className);
        String body = getPojoBody(colunmList);
        GeneratorUtils.writeFile(filePath + className + javaTail, head + GeneratorUtils.getClassDoc(tableComment) + classHead + body + "}\n");
    }

    private String getPojoHead(String packageName) {
        return "package " + packageName + ";\n\n"
                + "import lombok.Getter;\n"
                + "import lombok.Setter;\n"
                + "import lombok.ToString;\n"
                + "import lombok.experimental.Accessors;\n\n"
                + "import java.time.LocalDateTime;\n\n";
    }

    private String getPojoClassHead(String className) {
        return "@Getter\n"
                + "@Setter\n"
                + "@ToString\n"
                + "@Accessors(chain = true)\n"
                + "public class " + className + " {\n";
    }

    private String getPojoBody(List<Column> colunmList) {
        StringBuilder body = new StringBuilder();
        for (Column column : colunmList) {
            body.append("\n    /**\n     * ")
                    .append(column.getColumnComment())
                    .append("\n     */\n    private ")
                    .append(column.getDataType()).append(" ")
                    .append(GeneratorUtils.convertToJavaName(column.getColumnName(), false))
                    .append(";\n");
        }
        return body.toString();
    }

    private void generateMapperInterface(String filePath, String packageName, String className, String modelClassName,
                                         String lowerModelClass, List<Column> columnList, Map<String, List<String>> uniqueKeyGroupMap,
                                         String primaryKeyColumnName, String tableComment, String modelPackage) throws MojoFailureException {
        String mapperClassPath = filePath + className + javaTail;
        File file = new File(mapperClassPath);
        if (file.exists()) {
            return;
        }
        Map<String, Column> columnMap = new HashMap<>(columnList.size());
        for (Column column : columnList) {
            columnMap.put(column.getColumnName(), column);
        }
        boolean hasUniqueKey = uniqueKeyGroupMap != null && !uniqueKeyGroupMap.isEmpty();
        String head = "package " + packageName + ";\n\n"
                + "import " + modelPackage + "." + modelClassName + ";\n"
                + "import org.apache.ibatis.annotations.Mapper;\n";
        if (hasUniqueKey) {
            head += "import org.apache.ibatis.annotations.Param;\n";
        }
        head += "\n";
        String classHead = "@Mapper\n" + "public interface " + className + " {\n";
        String primaryKeyParamName = GeneratorUtils.convertToJavaName(primaryKeyColumnName, false);
        StringBuilder body = new StringBuilder("\n"
                + "    /**\n"
                + "     * 插入记录\n"
                + "     *\n"
                + "     * @param " + lowerModelClass + " 记录对象\n"
                + "     * @return 影响行数\n"
                + "     */\n"
                + "    int insert(" + modelClassName + " " + lowerModelClass + ");\n\n"
                + "    /**\n"
                + "     * 更新记录\n"
                + "     *\n"
                + "     * @param " + lowerModelClass + " 记录对象\n"
                + "     * @return 影响行数\n"
                + "     */\n"
                + "    int update(" + modelClassName + " " + lowerModelClass + ");\n\n"
                + "    /**\n"
                + "     * 根据主键查询记录\n"
                + "     *\n"
                + "     * @param " + primaryKeyParamName + " 主键\n"
                + "     * @return 记录对象\n"
                + "     */\n"
                + "    " + modelClassName + " selectById(" + columnMap.get(primaryKeyColumnName).getDataType() + " " + primaryKeyParamName + ");\n");
        if (hasUniqueKey) {
            for (Map.Entry<String, List<String>> entry : uniqueKeyGroupMap.entrySet()) {
                int maxLength = 0;
                StringBuilder methodName = new StringBuilder("selectBy");
                List<String> uniqueKeyGroup = entry.getValue();
                for (String column : uniqueKeyGroup) {
                    String javaName = GeneratorUtils.convertToJavaName(column, true);
                    maxLength = Math.max(maxLength, javaName.length());
                    methodName.append(javaName).append("And");
                }
                StringBuilder docParam = new StringBuilder();
                StringBuilder paramStr = new StringBuilder();
                for (String column : uniqueKeyGroup) {
                    String columnName = GeneratorUtils.convertToJavaName(column, false);
                    docParam.append("     * @param ").append(columnName)
                            .append(repeatBlank(maxLength - columnName.length() + 1))
                            .append(columnMap.get(column).getColumnComment()).append("\n");
                    paramStr.append("@Param(\"").append(columnName).append("\") ")
                            .append(columnMap.get(column).getDataType()).append(" ")
                            .append(columnName).append(", ");
                }
                body.append("\n"
                                + "    /**\n"
                                + "     * 根据唯一索引查询记录\n"
                                + "     *\n")
                        .append(docParam)
                        .append("     * @return 记录对象\n")
                        .append("     */\n")
                        .append("    ")
                        .append(modelClassName)
                        .append(" ").append(methodName, 0, methodName.length() - 3)
                        .append("(").append(paramStr, 0, paramStr.length() - 2).append(");\n");
            }
        }
        GeneratorUtils.writeFile(mapperClassPath, head + GeneratorUtils.getClassDoc(tableComment + "Mapper") + classHead + body + "}\n");
    }

    private void generateMapperXml(String filePath, String mapperPackageName, String mapperClassName, String modelReference,
                                   List<Column> columnList, String tableName, Map<String, List<String>> uniqueKeyGroupMap,
                                   String primaryKeyColumnName,
                                   List<String> insertIgnoreList) throws MojoFailureException {
        String mapperXmlPath = filePath + mapperClassName + xmlTail;
        File file = new File(mapperXmlPath);
        StringBuilder insertColumnStr = new StringBuilder();
        StringBuilder insertParamStr = new StringBuilder();
        StringBuilder updateSetStr = new StringBuilder();
        String insertHead = "        INSERT INTO " + tableName + " (";
        String valueHead = "        VALUES (";
        String insertPreBlank = repeatBlank(insertHead.length());
        String valuePreBlank = repeatBlank(valueHead.length());
        for (Column column : columnList) {
            String columnName = column.getColumnName();
            String sqlColumnName = GeneratorUtils.KEY_WORD_LIST.contains(columnName.toUpperCase()) ? "`" + columnName + "`" : columnName;
            String javaName = GeneratorUtils.convertToJavaName(columnName, false);
            if (!primaryKeyColumnName.equals(columnName)) {
                updateSetStr.append("\n            <if test=\"").append(javaName).append(" != null\">\n")
                        .append("                ").append(sqlColumnName).append(" = #{").append(javaName).append("},\n")
                        .append("            </if>");
            }
            if (insertIgnoreList.contains(columnName)) {
                continue;
            }
            insertColumnStr.append(sqlColumnName).append(",\n").append(insertPreBlank);
            insertParamStr.append("#{").append(javaName).append(",jdbcType=").append(column.getJdbcType()).append("}").append(",\n").append(valuePreBlank);
        }
        String insertInner = "\n" + insertHead + insertColumnStr.substring(0, insertColumnStr.length() - 2 - insertHead.length()) + ")"
                + "\n" + valueHead + insertParamStr.substring(0, insertParamStr.length() - 2 - valueHead.length()) + ")" + "\n    ";
        String updateInner = "\n        UPDATE " + tableName + "\n"
                + "        <set>" + updateSetStr + "\n"
                + "        </set>\n"
                + "        WHERE " + primaryKeyColumnName + " = #{" + GeneratorUtils.convertToJavaName(primaryKeyColumnName, false) + "}\n    ";
        if (!file.exists()) {
            String head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
                    + "<!DOCTYPE mapper PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\" \"http://mybatis.org/dtd/mybatis-3-mapper.dtd\">\n"
                    + "<mapper namespace=\"" + mapperPackageName + "." + mapperClassName + "\">\n\n";
            String insert = "    <insert id=\"insert\" parameterType=\"" + modelReference + "\">" + insertInner + "</insert>\n\n";
            String update = "    <update id=\"update\" parameterType=\"" + modelReference + "\">" + updateInner + "</update>\n\n";
            String selectById = "    <select id=\"selectById\" resultType=\"" + modelReference
                    + "\">\n        SELECT *"
                    + "\n        FROM " + tableName
                    + "\n        WHERE " + primaryKeyColumnName + " = #{" + GeneratorUtils.convertToJavaName(primaryKeyColumnName, false) + "}" + "\n"
                    + "    </select>\n\n";
            StringBuilder selectByUniqueKey = new StringBuilder();
            for (Map.Entry<String, List<String>> entry : uniqueKeyGroupMap.entrySet()) {
                StringBuilder methodName = new StringBuilder("selectBy");
                StringBuilder condition = new StringBuilder();
                List<String> uniqueKeyGroup = entry.getValue();
                for (String columnName : uniqueKeyGroup) {
                    String sqlColumnName = GeneratorUtils.KEY_WORD_LIST.contains(columnName.toUpperCase()) ? "`" + columnName + "`" : columnName;
                    methodName.append(GeneratorUtils.convertToJavaName(columnName, true)).append("And");
                    condition.append(" ").append(sqlColumnName).append(" = ").append("#{").append(GeneratorUtils.convertToJavaName(columnName,
                            false)).append("}").append("\n          AND");
                }
                selectByUniqueKey.append("    <select id=\"").append(methodName, 0, methodName.length() - 3).append("\" resultType=\"")
                        .append(modelReference).append("\">\n        SELECT *\n        FROM ").append(tableName).append("\n"
                                + "        WHERE").append(condition, 0, condition.length() - 14).append("\n    </select>\n\n");
            }
            GeneratorUtils.writeFile(mapperXmlPath, head + insert + update + selectById + selectByUniqueKey + "</mapper>");
        } else {
            changeExist(file, insertInner, updateInner);
        }
    }

    private void changeExist(File file, String insertInner, String updateInner) {
        try {
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            Document document = documentBuilder.parse(file);
            document.setXmlStandalone(true);
            NodeList nodeList = document.getElementsByTagName("mapper");
            Node node = nodeList.item(0);
            NodeList childNodeList = node.getChildNodes();
            for (int i = 0, length = childNodeList.getLength(); i < length; i++) {
                Node item = childNodeList.item(i);
                NamedNodeMap attributes = item.getAttributes();
                if (attributes == null) {
                    continue;
                }
                String nodeId = attributes.getNamedItem("id").getNodeValue();
                switch (item.getNodeName()) {
                    case "update":
                        if ("update".equals(nodeId)) {
                            item.replaceChild(document.createCDATASection(updateInner), item.getFirstChild());
                        }
                        break;
                    case "insert":
                        if ("insert".equals(nodeId)) {
                            item.setTextContent(insertInner);
                        }
                        break;
                    default:
                        break;
                }
            }
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name());
            transformer.setOutputProperty(OutputKeys.INDENT, "no");
            DOMImplementation domImpl = document.getImplementation();
            DocumentType doctype = domImpl.createDocumentType("mapper",
                    "-//mybatis.org//DTD Mapper 3.0//EN",
                    "http://mybatis.org/dtd/mybatis-3-mapper.dtd");
            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, doctype.getPublicId());
            transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doctype.getSystemId());
            transformer.transform(new DOMSource(document), new StreamResult(file));
        } catch (IOException | ParserConfigurationException | SAXException | TransformerException e) {
            getLog().error(e);
        }
    }
}
